using System;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Design;
using System.ComponentModel;
using System.Runtime.Serialization;
using System.Runtime.InteropServices;

using SharpGL.SceneGraph.Helpers;

namespace SharpGL.SceneGraph.Assets
{
	/// <summary>
	/// A Texture object is simply an array of bytes. It has OpenGL functions, but is
	/// not limited to OpenGL, so DirectX or custom library functions could be later added.
	/// </summary>
	[Editor(typeof(NETDesignSurface.Editors.UITextureEditor), typeof(UITypeEditor))]
	[TypeConverter(typeof(System.ComponentModel.ExpandableObjectConverter))]
    [DataContract(/*IsReference = true, */Name = "Texture", Namespace = "SharpGL.SceneGraph")]
    public class Texture2D : Asset, IBindable, IHasOpenGLContext, ICloneable, INotifyContextChanged, INotifyPropertyChanged
    {
        #region Private Consts

        private static readonly string defaultName = "2D texture";

        private static readonly OpenGL defaultCurrentOpenGLContext = null;
        private static readonly uint defaultTextureName = 0;
        private static readonly int defaultHeight = 0;
        private static readonly int defaultWidth = 0;
        private static readonly Bitmap defaultBitmap = null;
        private static readonly string defaultFileName = null;
        private static readonly bool defaultUseMipmaps = false;
        private static readonly byte[] defaultPixelData = null;

        #endregion Private Consts

        #region Private Fields

        /// <summary>
        /// Current OpenGL context
        /// </summary>
        [IgnoreDataMember()]
        private OpenGL currentOpenGLContext = null;

        /// <summary>
        /// This is for OpenGL textures, it is the unique ID for the OpenGL texture.
        /// </summary>
        [IgnoreDataMember()]
        private uint textureName = 0;

        /// <summary>
        /// The height of the texture image.
        /// </summary>
        [IgnoreDataMember()]
        private int height = 0;

        /// <summary>
        /// The width of the texture image.
        /// </summary>
        [IgnoreDataMember()]
        private int width = 0;

        /// <summary>
        /// The texture image.
        /// </summary>
        [IgnoreDataMember()]
        private Bitmap bitmap = null;

        /// <summary>
        /// The texture image file name.
        /// </summary>
        [DataMember()]
        private string fileName = null;

        /// <summary>
        /// If true create the texture using mipmaps.
        /// </summary>
        [DataMember()]
        private bool useMipmaps = false;

        /// <summary>
        /// This is an array of bytes (r, g, b, a) that represent the pixels in this
        /// texture object.
        /// </summary>
        [IgnoreDataMember()]
        private byte[] pixelData = null;

        #endregion Private Fields

        #region Private Properties

        /// <summary>
        /// This property used only for XML serializetion.
        /// </summary>
        [DataMember(Name = "Image")]
        private byte[] SerializedBitmap
        {
            get
            { // serialize 
                if (bitmap == null)
                    return null;
                using (MemoryStream ms = new MemoryStream())
                {
                    bitmap.Save(ms, ImageFormat.Bmp);
                    return ms.ToArray();
                }
            }
            set
            { // deserialize 
                if (value == null)
                {
                    bitmap = null;
                }
                else
                {
                    using (MemoryStream ms = new MemoryStream(value))
                    {
                        bitmap = new Bitmap(ms);

                        height = bitmap.Height;
                        width = bitmap.Width;
                    }
                }
            }
        }

        #endregion Private Fields

        #region Protected Methods

        /// <summary>
        /// Call this function to do the ContextChanged event.
        /// </summary>
        protected void NotifyContextChanged(object sender, ContextChangeAction action)
        {
            var handler = ContextChanged;
            if (handler != null)
                handler(sender, new ContextChangeEventArgs(action));
        }

        /// <summary>
        /// This function creates the texture from an image. 
        /// </summary>
        /// <param name="gl">The OpenGL object.</param>
        /// <param name="image">The image.</param>
        /// <param name="useMipmaps">If true create the texture using mipmaps.</param>
        /// <returns>OpenGL object handle.</returns>
        protected virtual uint InternalCreate(OpenGL gl, Bitmap image, bool useMipmaps)
        {
            uint[] textureArray = new uint[1] { uint.MaxValue };

            if (OpenGL.IsValidContext(gl))
            {
                //  Generate a texture identifier.
                gl.GenTextures(1, textureArray);

                if (image != null)
                {
                    //	Get the maximum texture size supported by OpenGL.
                    int[] textureMaxSize = { 0 };
                    gl.GetInteger(OpenGL.GL_MAX_TEXTURE_SIZE, textureMaxSize);

                    //	Find the target width and height sizes, which is just the highest
                    //	posible power of two that'll fit into the image.
                    int textureWidth = image.Width < textureMaxSize[0] ? MathHelper.RoundDownToPowerOf2(image.Width) : textureMaxSize[0];
                    int textureHeight = image.Height < textureMaxSize[0] ? MathHelper.RoundDownToPowerOf2(image.Height) : textureMaxSize[0];

                    Image textureImage = null;

                    //  If need to scale, do so now.
                    if (image.Width != textureWidth || image.Height != textureHeight)
                    {
                        //  Resize the image.
                        textureImage = image.GetThumbnailImage(textureWidth, textureHeight, null, IntPtr.Zero);

                        //  Reset texture image.
                        //image.Dispose();
                        image = (Bitmap)textureImage;
                    }

                    //	Bind our texture object (make it the current texture).
                    gl.BindTexture(OpenGL.GL_TEXTURE_2D, textureArray[0]);

                    //  Lock the image bits (so that we can pass them to OpenGL).
                    BitmapData bitmapData = image.LockBits(new Rectangle(0, 0, image.Width, image.Height),
                                                        ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);

                    //  Set the image data.
                    if (useMipmaps)
                    {
                        gl.Build2DMipmaps(OpenGL.GL_TEXTURE_2D,
                                                    3,
                                                    textureWidth,
                                                    textureHeight,
                                                    OpenGL.GL_BGRA,
                                                    OpenGL.GL_UNSIGNED_BYTE,
                                                    bitmapData.Scan0);
                    }
                    else
                    {
                        gl.TexImage2D(OpenGL.GL_TEXTURE_2D, 0, (int)OpenGL.GL_RGBA,
                            textureWidth, textureHeight, 0, OpenGL.GL_BGRA, OpenGL.GL_UNSIGNED_BYTE,
                            bitmapData.Scan0);
                    }

                    //  Unlock the image.
                    image.UnlockBits(bitmapData);

                    //  If we create resized image dispose it.
                    if (textureImage != null)
                        textureImage.Dispose();
                }
            }

            return textureArray[0];
        }

        protected override void OnDeserializing()
        {
            base.OnDeserializing();

            currentOpenGLContext = defaultCurrentOpenGLContext;
            textureName = defaultTextureName;
            pixelData = (defaultPixelData != null ? (byte[])defaultPixelData.Clone() : defaultPixelData);
        }

        #endregion Protected Methods

        #region Public Constructors

        public Texture2D()
        {
            Name = (defaultName != null ? defaultName : "");
            
            currentOpenGLContext = defaultCurrentOpenGLContext;
            textureName = defaultTextureName;
            height = defaultHeight;
            width = defaultWidth;
            bitmap = (defaultBitmap != null ? (Bitmap)defaultBitmap.Clone() : defaultBitmap);
            fileName = (defaultFileName != null ? (string)defaultFileName.Clone() : defaultFileName);
            useMipmaps = defaultUseMipmaps;
            pixelData = (defaultPixelData != null ? (byte[])defaultPixelData.Clone() : defaultPixelData);
        }

        public Texture2D(string path)
            : this()
        {
            this.fileName = path;
        }

        public Texture2D(Bitmap image)
            : this()
        {
            this.bitmap = (Bitmap)image.Clone();
        }

        #endregion Public Constructors

        #region Public Methods

        /// <summary>
        /// This function creates the underlying OpenGL object.
        /// </summary>
        /// <param name="gl">The OpenGL object.</param>
        /// <returns>True if the texture was successfully created.</returns>
        public virtual bool Create(OpenGL gl)
        {
            return Create(gl, (Bitmap)null);
        }

        /// <summary>
        /// This function creates the texture from an image file.
        /// </summary>
        /// <param name="gl">The OpenGL object.</param>
        /// <param name="path">The path to the image file.</param>
        /// <param name="useMipmaps">If true create the texture using mipmaps.</param>
        /// <returns>True if the texture was successfully loaded.</returns>
        public virtual bool Create(OpenGL gl, string path)
        {
            //  Try and load the bitmap. Return false on failure.
            using (Bitmap image = new Bitmap(path))
            {
                if (image == null)
                    return false;

                //  Call the main create function.
                return Create(gl, image);
            }
        }

        /// <summary>
        /// This function creates the texture from an image. 
        /// </summary>
        /// <param name="gl">The OpenGL object.</param>
        /// <param name="image">The image.</param>
        /// <param name="useMipmaps">If true create with mipmaps.</param>
        /// <returns>True if the texture was successfully created.</returns>
        public virtual bool Create(OpenGL gl, Bitmap image)
        {
            if (!OpenGL.Equals(this.currentOpenGLContext, gl))
            {
                if (OpenGL.IsValidContext(this.currentOpenGLContext))
                    throw new Exception("Can't create '" +
                                        Name +
                                        "' in context '" +
                                        (OpenGL.IsValidContext(gl) ? gl.Renderer : "Null") +
                                        "' - it is already created in '" +
                                        (OpenGL.IsValidContext(this.currentOpenGLContext) ? this.currentOpenGLContext.Renderer : "Null") +
                                        "' OpenGL context.");

                if (OpenGL.IsValidContext(gl))
                {
                    this.textureName = InternalCreate(gl, image, useMipmaps);
                    NotifyPropertyChanged("TextureName");
                    
                    //  Store new texture width and height.
                    this.height = image == null ? 0 : image.Height;
                    NotifyPropertyChanged("Height");
                    this.width = image == null ? 0 : image.Width;
                    NotifyPropertyChanged("Width");

                    this.currentOpenGLContext = gl;
                    NotifyContextChanged(this, ContextChangeAction.CreateInContext);
                }
            }

            return true;
        }

        /// <summary>
        /// This function destroys the OpenGL object that is a representation of this texture.
        /// </summary>
        public virtual void Destroy()
        {
            if (OpenGL.IsValidContext(this.currentOpenGLContext))
            {
                NotifyContextChanged(this, ContextChangeAction.DestroyInContext);

                //	Destroy the texture object.
                this.currentOpenGLContext.DeleteTextures(1, new uint[1] { this.textureName });
                this.textureName = 0;

                //  Destroy the pixel data.
                this.pixelData = null;

                //  ResetAllAttributes width and height.
                this.width = this.height = 0;

                this.currentOpenGLContext = null;
            }
        }

        /// <summary>
        /// Create in the context of the supplied OpenGL instance.
        /// </summary>
        /// <param name="gl">The OpenGL instance.</param>
        public virtual void CreateInContext(OpenGL gl)
        {
            if (this.bitmap != null)
            {
                Create(gl, this.bitmap);
            }
            else if (this.fileName != null)
            {
                Create(gl, this.fileName);
            }
            else
            {
                Create(gl);
            }
        }

        /// <summary>
        /// Destroy in the current OpenGL instance.
        /// </summary>
        public virtual void DestroyInContext()
        {
            Destroy();
        }

        /// <summary>
        /// Bind to the specified OpenGL instance.
        /// </summary>
        /// <param name="gl">The OpenGL instance.</param>
        public virtual void Bind(OpenGL gl)
        {
            if (OpenGL.IsValidContext(this.currentOpenGLContext))
            {
                if (!OpenGL.Equals(this.currentOpenGLContext, gl))
                    throw new Exception("Can't bind '" +
                                        Name +
                                        "' in context '" +
                                        (OpenGL.IsValidContext(gl) ? gl.Renderer : "Null") +
                                        "' - it is created in '" +
                                        (OpenGL.IsValidContext(this.currentOpenGLContext) ? this.currentOpenGLContext.Renderer : "Null") +
                                        "' OpenGL context.");

                //	Bind our texture object (make it the current texture).
                gl.BindTexture(OpenGL.GL_TEXTURE_2D, this.textureName);
            }
        }

        public virtual void Push(OpenGL gl)
        {
            if (OpenGL.IsValidContext(gl))
            {
                //  Push texture attributes.
                gl.PushAttrib(Enumerations.AttributeMask.Texture);
            }
 
            //  Bind the texture.
            Bind(gl); 
        }

        public virtual void Pop(OpenGL gl)
        {
            if (OpenGL.IsValidContext(gl))
            {
                //  Pop attributes.
                gl.PopAttrib();
            }
        }

        /// <summary>
        /// Creates a new object that is a copy of the current instance.
        /// </summary>
        /// <returns>
        /// a new object that is a copy of this instance.
        /// </returns>
        public virtual object Clone()
        {
            Texture2D newTexture = new Texture2D();

            newTexture.bitmap = (this.bitmap != null ? (Bitmap)this.bitmap.Clone() : null);
            newTexture.fileName = (this.fileName != null ? this.fileName : null);
            newTexture.useMipmaps = this.useMipmaps;
            pixelData = (this.pixelData != null ? (byte[])this.pixelData.Clone() : null);

            return newTexture;
        }

        /// <summary>
        /// This function (attempts) to make a bitmap from the raw data. The fact that
        /// the byte array is a managed type makes it slightly more complicated.
        /// </summary>
        /// <returns>The texture object as a Image.</returns>
        public virtual Bitmap ToBitmap()
        {
            if (this.bitmap != null)
                return (Bitmap)this.bitmap.Clone();
            else if (this.fileName != null)
                return new Bitmap(this.fileName);
            else if (this.pixelData != null)
            {
                //  Pin the pixel data.
                GCHandle handle = GCHandle.Alloc(this.pixelData, GCHandleType.Pinned);

                //  Create the bitmap.
                Bitmap image = new Bitmap(this.width, this.height, this.width * 4,
                        PixelFormat.Format32bppRgb, handle.AddrOfPinnedObject());

                //  Free the data.
                handle.Free();

                //  Return the bitmap.
                return image;
            }
            else
                return null;
        }

        #endregion Public Methods

        #region Public Events

        [Description("Called when OpenGL context has been changed."), Category("SharpGL")]
        public event ContextChangeEventHandler ContextChanged;

        #endregion Public Events

        #region Public Properties

        /// <summary>
        /// Current OpenGL context
        /// </summary>
        [Browsable(false)]
        public OpenGL CurrentOpenGLContext
        {
            get { return currentOpenGLContext; }
        }
        
        /// <summary>
        /// Gets the name of the texture.
        /// </summary>
        /// <value>
        /// The name of the texture.
        /// </value>
		[Description("The internal texture code."), Category("Texture")]
        public uint TextureName
		{
            get { return textureName; }
		}

        /// <summary>
        /// Gets the height of the texture.
        /// </summary>
        /// <value>
        /// The height of the texture.
        /// </value>
        [Description("The texture height."), Category("Texture")]
        public int Height
        {
            get { return height; }
        }

        /// <summary>
        /// Gets the width of the texture.
        /// </summary>
        /// <value>
        /// The width of the texture.
        /// </value>
        [Description("The texture width."), Category("Texture")]
        public int Width
        {
            get { return width; }
        }

        /// <summary>
        /// Gets and sets the texture image.
        /// </summary>
        /// <value>
        /// The texture image (if not null the CreateInContext() function use with bitmap).
        /// </value>
        [Description("The texture image."), Category("Texture")]
        public Bitmap Image
        {
            get { return bitmap; }
            set
            {
                OpenGL currentContext = this.currentOpenGLContext;

                if (OpenGL.IsValidContext(this.currentOpenGLContext))
                    DestroyInContext();

                this.bitmap = (value != null ? (Bitmap)value.Clone() : value);
                this.fileName = null;

                NotifyPropertyChanged("FileName");

                if (OpenGL.IsValidContext(currentContext/*this.currentOpenGLContext*/))
                    CreateInContext(currentContext/*this.currentOpenGLContext*/);

                NotifyPropertyChanged("Image");
            }
        }

        /// <summary>
        /// Gets and sets the texture image file name.
        /// </summary>
        /// <value>
        /// The texture image file name (if not null the CreateInContext()
        /// function use with file name to load bitmap).
        /// </value>
        [Description("The texture image."), Category("Texture")]
        public string FileName
        {
            get { return fileName; }
            set
            {
                if (OpenGL.IsValidContext(this.currentOpenGLContext))
                    DestroyInContext();

                this.fileName = value;
                this.bitmap = null;

                NotifyPropertyChanged("Image");

                if (OpenGL.IsValidContext(this.currentOpenGLContext))
                    CreateInContext(this.currentOpenGLContext);

                NotifyPropertyChanged("FileName");
            }
        }

        /// <summary>
        /// Gets and sets the texture image file name.
        /// </summary>
        /// <value>
        /// The texture image file name (if not null the CreateInContext()
        /// function use with file name to load bitmap).
        /// </value>
        [Description("The texture image."), Category("Texture")]
        public bool UseMipmaps
        {
            get { return useMipmaps; }
            set
            {
                if (this.useMipmaps != value)
                {
                    if (OpenGL.IsValidContext(this.currentOpenGLContext))
                        DestroyInContext();

                    this.useMipmaps = value;

                    if (OpenGL.IsValidContext(this.currentOpenGLContext))
                        CreateInContext(this.currentOpenGLContext);

                    NotifyPropertyChanged("UseMipmaps");
                }
            }
        }

        #endregion Public Properties
    }
}
